/******************************************************************************

    Copyright (C) 2006 - 2011 Alessio Stalla (alessiostalla@gmail.com)

    This file is part of PrettyProlog.

    PrettyProlog is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    PrettyProlog is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with PrettyProlog; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    See the file COPYING for further details.

 *****************************************************************************/

package prettyprolog.datatypes;

import java.io.InvalidObjectException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * This class is used to produce new symbols.
 */
public class Namespace extends AbstractTerm {

    private static final long serialVersionUID = 1L;
    private final Map<String, Symbol> symtable = new HashMap<String, Symbol>();
	/**
	 * Infix symbols. Sorted to aid in parsing: longer symbols come first, so e.g. == is encountered before =
	 * when iterating through the keys. 
	 */
	private static final SortedSet<Symbol> infixSymbols = new TreeSet<Symbol>(new SymbolNameComparator());
	private final String name;
	private final Namespace[] parents;
	private static int symcount = 0;
	private static String prefix = "_";
	private static ConcurrentMap<String, Namespace> namespaces = new ConcurrentHashMap<String, Namespace>();
	private static final ThreadLocal<Namespace> currentNamespace = new ThreadLocal<Namespace>();
	
	public Namespace(String name, Namespace... parents) {
	    this.name = name;
	    this.parents = parents;
	    Namespace other = namespaces.putIfAbsent(name, this);
	    if(other != null) {
	        throw new IllegalArgumentException("Another symbol table is already registered with name " + name + ": " + other);
	    }
	}

	/**
	 * Creates a new Symbol with print name s.
	 * 
	 * @param s the print name of the symbol.
	 * @return a Symbol with the specified print name. It is guaranteed that two
	 *         calls to intern() with the same (i.e. equals()) parameter will
	 *         return the same symbol.
	 */
	public synchronized Symbol intern(String s) {
		Symbol sym = findSymbol(s);
		if (sym == null) {
			sym = new Symbol(s, this);
			symtable.put(s, sym);
		}
		return sym;
	}
	
	//TODO no synchronized, read/write lock instead?
	public synchronized Symbol findSymbol(String s) {
	    Symbol sym = symtable.get(s);
	    if(sym == null) {
	        for(Namespace parent : parents) {
	            sym = parent.findSymbol(s);
	            if(sym != null) {
	                return sym;
	            }
	        }
	    }
        return sym;
	}
	
	public synchronized Symbol internInfix(String s) {
		Symbol sym = intern(s);
		sym.setInfix(true);
		return sym;
	}

	public static synchronized Symbol infix(Symbol sym, boolean flag) {
		if(flag) {
			infixSymbols.add(sym);
		} else {
			infixSymbols.remove(sym);
		}
		return sym;
	}
	
	public synchronized Symbol importSymbol(Symbol s) {
	    return importSymbol(s, s.getName());
	}

	public synchronized Symbol importSymbol(Symbol s, String name) {
	    Symbol old = findSymbol(name);
        if(old != null) {
            symtable.put(name, s);
        } else if(old != s) {
            throw new RuntimeException("Symbol conflict: tried to import " + s + " as " + name + " in " + this + " but " + name + " already names symbol " + old);
        }
        return s;
    }
	
	public static Namespace get(String name) {
	    return namespaces.get(name);
	}
	
	public void unregister() {
	    namespaces.remove(name);
	}

    /**
	 * Gets the prefix used to construct the print name of the symbols returned
	 * by the gensym() method.
	 * 
	 * @return the prefix.
	 */
	public static String getGensymPrefix() {
		return prefix;
	}

	/**
	 * Sets the prefix used to construct the print name of the symbols returned
	 * by the gensym() method.
	 */
	public static void setGensymPrefix(String s) {
		prefix = s;
	}

	/**
	 * Gets the number used to construct the print name of the symbols returned
	 * by the gensym() method.
	 * 
	 * @return the gensym count.
	 */
	public static int getGensymCount() {
		return symcount;
	}

	/**
	 * Sets the number used to construct the print name of the symbols returned
	 * by the gensym() method.
	 */
	public static void setGensymCount(int n) {
		symcount = n;
	}

	/**
	 * Creates a new Symbol that is guaranteed to be unique. The symbol will
	 * have a printed representation consisting of a prefix followed by a
	 * number.<br />
	 * Note that two symbols can be different even if their print name is the
	 * same.
	 */
	public static Symbol gensym() {
		return new Symbol(prefix + (symcount++), null);
	}
		
	@SuppressWarnings("serial")
	protected static class SymbolNameComparator implements Comparator<Symbol>, Serializable {

		public int compare(Symbol o1, Symbol o2) {
			int cmp = Integer.signum(o2.getName().length() - o1.getName().length());
			if(cmp != 0) {
				return cmp;
			} else {
				return o1.getName().compareTo(o2.getName());
			}
		}
	}
	
	protected static class SerializedNamespace implements Serializable {
        private static final long serialVersionUID = Namespace.serialVersionUID;
        private final String name;
        private SerializedNamespace(String name) { this.name = name; }
        private Object readResolve() throws ObjectStreamException {
            Namespace st = Namespace.get(name);
            if(st != null) {
                return st;
            } else {
                throw new InvalidObjectException("No symbol table named " + name + " exists.");
            }
        }
	}
	
	private Object writeReplace() throws ObjectStreamException {
	    return new SerializedNamespace(name);
	}
	
	/**
	 * Returns the set of infix symbols. Do not modify the return value.
	 * @return the set of infix symbols.
	 */
	public static SortedSet<Symbol> getInfixSymbols() {
		return infixSymbols;
	}
	
	public String getName() {
        return name;
    }
	
	public static Namespace current() {
	    return currentNamespace.get();
	}
	
	public static void setCurrent(Namespace current) {
	    currentNamespace.set(current);
	}
	
}